import { type EmitEventListenr, type EmitEventMap, useEmit } from '../hooks/useEmit'
import { isObject, uuid } from '../utils/index'

export type WindowMessageSide = 'window' | 'content'

interface WindowMessageContext<T extends EmitEventMap = EmitEventMap, K extends keyof T = keyof T> {
  postMessage(type: K, data: T[K]): void
  onMessage(type: K, listener: EmitEventListenr<T[K]>, once?: boolean): void
  offMessage(type: K, listener: EmitEventListenr<T[K]>): void
}

interface WindowMessageChannelInit<T extends EmitEventMap = EmitEventMap, K extends keyof T = keyof T> {
  type: K
  data: T[K]
}

interface WindowMessageConnectInit {
  type: string
  origin: string
}

const ConnectEventKey = 'message:side:connect'

const MessgeEmitNamespace = Symbol('window-message')

class ConnectStorage {
  private originKey = 'message:side:parent'
  private childrenKey = 'message:side:child'
  private storage = sessionStorage
  public currentOrigin = uuid()

  constructor(private side: WindowMessageSide) {
    const { currentOrigin } = this
    if (side === 'window') {
      this.childOrigin = currentOrigin
      window.addEventListener('beforeunload', () => {
        this.childOrigin = null
      })
    } else {
      this.parentOrigin = currentOrigin
      window.addEventListener('beforeunload', () => {
        this.parentOrigin = null
      })
    }
  }

  get parentOrigin() {
    const { storage, originKey } = this
    return storage.getItem(originKey)
  }

  set parentOrigin(value: string | null) {
    const { storage, originKey } = this
    value ? storage.setItem(originKey, value) : storage.removeItem(originKey)
  }

  get childOrigin() {
    const { storage, childrenKey } = this
    return storage.getItem(childrenKey)
  }

  set childOrigin(value: string | null) {
    const { storage, childrenKey } = this
    value ? storage.setItem(childrenKey, value) : storage.removeItem(childrenKey)
  }
}

function inlineSizeWindowSide<T extends EmitEventMap>(): WindowMessageContext<T> {
  const storage = new ConnectStorage('window')

  let port2: MessagePort | null = null

  const { on: onMessage, off: offMessage, emit } = useEmit<T>(MessgeEmitNamespace)

  function bindProt(port: MessagePort) {
    port2 = port
    port.onmessage = (event) => {
      const { type, data } = event.data as WindowMessageChannelInit<T>
      emit(type, data)
    }

    console.log('window:message is connect')
  }

  function connect({ data, ports }: MessageEvent) {
    if (isObject<WindowMessageConnectInit>(data) && data.type === ConnectEventKey && storage.parentOrigin === data.origin && ports.length) {
      window.removeEventListener('message', connect)
      bindProt(ports[0])
    }
  }

  window.addEventListener('message', connect)

  return {
    onMessage,
    offMessage,
    postMessage(type, data) {
      port2?.postMessage({ type, data })
    },
  }
}

function inlineSizeContentSide<T extends EmitEventMap>(): WindowMessageContext<T> {
  const storage = new ConnectStorage('content')

  const { port1, port2 } = new MessageChannel()

  const { on: onMessage, off: offMessage, emit } = useEmit<T>(MessgeEmitNamespace)

  port1.onmessage = (event) => {
    const { type, data } = event.data as WindowMessageChannelInit<T>
    emit(type, data)
  }

  const timer = setInterval(function connect() {
    if (storage.childOrigin) {
      clearInterval(timer)
      const data: WindowMessageConnectInit = { type: ConnectEventKey, origin: storage.parentOrigin! }
      window.postMessage(data, location.origin, [port2])
    }
  }, 100)

  return {
    onMessage,
    offMessage,
    postMessage(type, data) {
      port1?.postMessage({ type, data })
    },
  }
}

export function useWindowMessage<T extends EmitEventMap>(side: WindowMessageSide): WindowMessageContext<T> {
  return window.windowMessageContext ?? (window.windowMessageContext = side === 'window' ? inlineSizeWindowSide<T>() : inlineSizeContentSide<T>())
}
